//-----------------------------------------------------------------------
// <copyright file="EmulatorClientSocket.cs" company="Google Inc.">
// Copyright 2016 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
//-----------------------------------------------------------------------

// This class is only used in the Editor, so make sure to only compile it on that platform.
// Additionally, If this class is compiled on Android then Unity will insert the INTERNET permission
// into the manifest because of the reference to the type TCPClient. Excluding this class in the android
// build ensures that it is only included if the developer using the SDK actually uses INTERNET related services.
// This MonoBehaviour is only ever instantiated dynamically, so it is fine that it is only compiled in the Editor,
// Otherwise it would cause serialization issues.
#if UNITY_EDITOR

using UnityEngine;
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;

using proto;

/// @cond
namespace Gvr.Internal
{
    public enum EmulatorClientSocketConnectionState
    {
        Disconnected = 0,
        Connecting = 1,
        Connected = 2,
    }

    class EmulatorClientSocket : MonoBehaviour
    {
        private static readonly int kPhoneEventPort = 7003;
        private const int kSocketReadTimeoutMillis = 5000;

        // Minimum interval, in seconds, between attempts to reconnect the socket.
        private const float kMinReconnectInterval = 1f;

        private TcpClient phoneMirroringSocket;

        private Thread phoneEventThread;

        private volatile bool shouldStop = false;

        // Flag used to limit connection state logging to initial failure and successful reconnects.
        private volatile bool lastConnectionAttemptWasSuccessful = true;

        private EmulatorManager phoneRemote;

        public EmulatorClientSocketConnectionState connected { get; private set; }

        public void Init(EmulatorManager remote)
        {
            phoneRemote = remote;

            if (EmulatorConfig.Instance.PHONE_EVENT_MODE != EmulatorConfig.Mode.OFF)
            {
                phoneEventThread = new Thread(phoneEventSocketLoop);
                phoneEventThread.IsBackground = true;
                phoneEventThread.Start();
            }
        }

        private void phoneEventSocketLoop()
        {
            while (!shouldStop)
            {
                long lastConnectionAttemptTime = DateTime.Now.Ticks;
                try
                {
                    phoneConnect();
                }
                catch (Exception e)
                {
                    if (lastConnectionAttemptWasSuccessful)
                    {
                        Debug.LogWarningFormat("{0}\n{1}", e.Message, e.StackTrace);

                        // Suppress additional failures until we have successfully reconnected.
                        lastConnectionAttemptWasSuccessful = false;
                    }
                }

                // Wait a while in order to enforce the minimum time between connection attempts.
                TimeSpan elapsed = new TimeSpan(DateTime.Now.Ticks - lastConnectionAttemptTime);
                float toWait = kMinReconnectInterval - (float)elapsed.TotalSeconds;
                if (toWait > 0)
                {
                    Thread.Sleep((int)(toWait * 1000));
                }
            }
        }

        private void phoneConnect()
        {
            string addr = EmulatorConfig.Instance.PHONE_EVENT_MODE == EmulatorConfig.Mode.USB
        ? EmulatorConfig.USB_SERVER_IP : EmulatorConfig.WIFI_SERVER_IP;

            try
            {
                if (EmulatorConfig.Instance.PHONE_EVENT_MODE == EmulatorConfig.Mode.USB)
                {
                    setupPortForwarding(kPhoneEventPort);
                }

                TcpClient tcpClient = new TcpClient(addr, kPhoneEventPort);
                connected = EmulatorClientSocketConnectionState.Connecting;
                ProcessConnection(tcpClient);
                tcpClient.Close();
            }
            finally
            {
                connected = EmulatorClientSocketConnectionState.Disconnected;
            }
        }

        private void setupPortForwarding(int port)
        {
#if !UNITY_WEBPLAYER
            string adbCommand = string.Format("adb forward tcp:{0} tcp:{0}", port);
            System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
            string processFilename;
            string processArguments;
            int kExitCodeCommandNotFound;

            if (Application.platform == RuntimePlatform.WindowsEditor ||
                   Application.platform == RuntimePlatform.WindowsPlayer)
            {
                processFilename = "CMD.exe";
                processArguments = @"/k " + adbCommand + " & exit";

                // See "Common Error Lookup Tool" (https://www.microsoft.com/en-us/download/details.aspx?id=985)
                // MSG_DIR_BAD_COMMAND_OR_FILE (cmdmsg.h)
                kExitCodeCommandNotFound = 9009; // 0x2331
            }
            else
            { // Unix
                processFilename = "bash";
                processArguments = string.Format("-l -c \"{0}\"", adbCommand);

                // "command not found" (see http://tldp.org/LDP/abs/html/exitcodes.html)
                kExitCodeCommandNotFound = 127;
            }

            System.Diagnostics.ProcessStartInfo myProcessStartInfo =
                new System.Diagnostics.ProcessStartInfo(processFilename, processArguments);
            myProcessStartInfo.UseShellExecute = false;
            myProcessStartInfo.RedirectStandardError = true;
            myProcessStartInfo.CreateNoWindow = true;
            myProcess.StartInfo = myProcessStartInfo;
            myProcess.Start();
            myProcess.WaitForExit();

            // Also wait for HasExited here, to avoid ExitCode access below occasionally throwing InvalidOperationException
            while (!myProcess.HasExited)
            {
                Thread.Sleep(1);
            }

            int exitCode = myProcess.ExitCode;
            string standardError = myProcess.StandardError.ReadToEnd();
            myProcess.Close();

            if (exitCode == 0)
            {
                // Port forwarding setup successfully.
                return;
            }

            if (exitCode == kExitCodeCommandNotFound)
            {
                // Caught by phoneEventSocketLoop.
                throw new Exception(
                    "Android Debug Bridge (`adb`) command not found." +
                    "\nVerify that the Android SDK is installed and that the directory containing" +
                    " `adb` is included in your PATH environment variable.");
            }

            // Caught by phoneEventSocketLoop.
            throw new Exception(
                string.Format(
                    "Failed to setup port forwarding." +
                    " Exit code {0} returned by process: {1} {2}\n{3}",
                    exitCode, processFilename, processArguments, standardError));
#endif  // !UNITY_WEBPLAYER
        }

        private void ProcessConnection(TcpClient tcpClient)
        {
            byte[] buffer = new byte[4];
            NetworkStream stream = tcpClient.GetStream();
            stream.ReadTimeout = kSocketReadTimeoutMillis;
            tcpClient.ReceiveTimeout = kSocketReadTimeoutMillis;
            while (!shouldStop)
            {
                int bytesRead = blockingRead(stream, buffer, 0, 4);
                if (bytesRead < 4)
                {
                    // Caught by phoneEventSocketLoop.
                    throw new Exception(
                        "Failed to read from controller emulator app event socket." +
                        "\nVerify that the controller emulator app is running.");
                }

                int msgLen = unpack32bits(correctEndianness(buffer), 0);

                byte[] dataBuffer = new byte[msgLen];
                bytesRead = blockingRead(stream, dataBuffer, 0, msgLen);
                if (bytesRead < msgLen)
                {
                    // Caught by phoneEventSocketLoop.
                    throw new Exception(
                        "Failed to read from controller emulator app event socket." +
                        "\nVerify that the controller emulator app is running.");
                }

                PhoneEvent proto =
                    PhoneEvent.CreateBuilder().MergeFrom(dataBuffer).Build();
                phoneRemote.OnPhoneEvent(proto);

                connected = EmulatorClientSocketConnectionState.Connected;

                if (!lastConnectionAttemptWasSuccessful)
                {
                    Debug.Log("Successfully connected to controller emulator app.");

                    // Log first failure after after successful read from event socket.
                    lastConnectionAttemptWasSuccessful = true;
                }
            }
        }

        private int blockingRead(NetworkStream stream, byte[] buffer, int index,
                                int count)
        {
            int bytesRead = 0;
            while (!shouldStop && bytesRead < count)
            {
                try
                {
                    int n = stream.Read(buffer, index + bytesRead, count - bytesRead);
                    if (n <= 0)
                    {
                        // Failed to read.
                        return -1;
                    }

                    bytesRead += n;
                }
                catch (IOException)
                {
                    // Read failed or timed out.
                    return -1;
                }
                catch (ObjectDisposedException)
                {
                    // Socket closed.
                    return -1;
                }
            }

            return bytesRead;
        }

        void OnDestroy()
        {
            shouldStop = true;

            if (phoneMirroringSocket != null)
            {
                phoneMirroringSocket.Close();
                phoneMirroringSocket = null;
            }

            if (phoneEventThread != null)
            {
                phoneEventThread.Join();
            }
        }

        private int unpack32bits(byte[] array, int offset)
        {
            int num = 0;
            for (int i = 0; i < 4; i++)
            {
                num += array[offset + i] << (i * 8);
            }

            return num;
        }

        static private byte[] correctEndianness(byte[] array)
        {
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(array);
            }

            return array;
        }
    }
}

/// @endcond
#endif  // UNITY_EDITOR
